在前面的章節中,我們已經詳細的解析了 React element 相關的核心概念以及建立的方法。不過當你去參考絕大多數的 React 專案的程式碼時,你會發現幾乎看不太到 React.createElment()
的蹤影,取而代之的是一種叫做「JSX」的語法。
JSX 的語法長得非常像 HTML 的標籤語法,甚至有不少 React 的教學資源或課程會宣稱「JSX 是把 HTML 寫在 JavaScript 中」。不過就本質上來說,這種說法其實錯的還蠻離譜的,而這也是許多 React 新手非常容易陷入的巨大誤解,進而讓許多人以為 JSX 是一種 React 特有的「黑魔法」,變成技術認知上的黑洞 — 你看得到它的外表,但是卻完全不知道其內在的本質是什麼東西。
以下就讓我們真正的從零解析 JSX 語法的本質到底是什麼,以及我們該如何理解並運用它。
讓我們先回到 React element。
雖然 React 的 createElement()
方法已經可以讓我們產生 React element 來方便且清楚的指定實際上想要的 DOM Tree 結構了,但是相較於以往我們習慣的以 HTML 標籤語法來表示 DOM 樹狀結構的體驗還是有段差距。
因此 React 提供了一種稱之為「JSX」的語法糖,能讓我們在定義 React element 的結構時有著相當類似於撰寫 HTML 語法的體驗。當我們開發時可以撰寫 JSX 語法,然後以專門的工具自動化的幫我們轉換成 React.createElement()
的語法。
我們從一個範例先來看看實際上的效果:
// 以普通的呼叫 React.createElement() 來定義 React element
const reactElement = React.createElement(
'div',
{ id: 'wrapper', className: 'foo' },
React.createElement(
'ul',
{ id: 'list-01' },
// 從第三個參數開始每個參數都是子元素
React.createElement('li', { className: 'list-item' }, 'item 1'),
// 第四個參數就會是第二個子元素
React.createElement('li', { className: 'list-item' }, 'item 2'),
// 第五個參數就會是第三個子元素
React.createElement('li', { className: 'list-item' }, 'item 3'),
),
React.createElement(
'button',
{ id: 'button1' },
'I am a button'
)
);
// 以 JSX 語法來定義 React element
const reactElementWithJSX = (
<div id="wrapper" className="foo">
<ul id="list-01">
<li className="list-item">item 1</li>
<li className="list-item">item 2</li>
<li className="list-item">item 3</li>
</ul>
<button id="button1">I am a button</button>
</div>
);
在以上的範例中,其實下面的 reactElementWithJSX
這段 JSX 語法也是會回傳一個 React element,並且這兩個 React element 的內容其實是完全相同的,使用了 JSX 語法的後者在經過開發工具的自動轉換之後就會分毫不差的變成前者,因此寫 JSX 語法其實就是在寫 React.createElement()
。
所以只要你願意的話,其實也可以像前面章節的範例一樣完全不使用 JSX 語法,以全部親自寫 React.createElement()
的方式來開發 React App。不過,以 JSX 語法來定義畫面的樹狀結構顯然更方便簡潔,也更接近我們以往所熟悉的 HTML 語法的體驗。因此,為了更好的程式碼可讀性與開發體驗,在開發 React App 的絕大多數時候我們都會推薦使用 JSX 語法來定義 React element。當然,你需要注意一些前面有提到過的 React element props 與 HTML 語法不一樣的地方,例如像是 class
⇒ className
。
React.createElement()
的替代語法,而不是在 JS 裡寫 HTML!因此,關於 「JSX 的本質是什麼」的結論是:
JSX 語法的本質完完全全就是 React.createElement()
方法的呼叫。
它長得很像 HTML 語法只是因為它被刻意設計成模仿 HTML 語法的撰寫和開發體驗,但是與 HTML 在本質上完全是不同的東西!
同樣的,你的 component function 中回傳了一段 JSX,也是代表你的 component function 回傳了一個 React element 的值:
function HelloWorld(props) {
return (
<div>
<h1>Hello world!</h1>
<h2>My name is {props.name}</h2>
</div>
);
}
// 經過 JSX 轉譯後:
function HelloWorld(props) {
return React.createElement(
'div',
null,
React.createElement('h1', null, 'Hello world!'),
React.createElement('h2', null, 'My name is ', props.name)
);
}
因此當你看到一段 JSX 語法時,它其實是在表達一個「值」。這個值既不是 HTML 字串也不是真實的 DOM element,而是呼叫 React.createElement()
方法的回傳值,也就是一個 React element。
然而問題來了,我們開發時撰寫的 JSX 語法是如何在瀏覽器的 JavaScript 引擎中能夠正常運作的呢?在沒有先進行轉換的情況下直接拿來執行的話,JSX 語法在正常的 JavaScript 執行環境中顯然是完全不合法的,只會造成 runtime 的錯誤:
因此我們得在實際拿到 runtime 環境中執行之前,先將這段程式碼進行轉換靜態的轉譯,將其中的 JSX 語法都替換成真正可運行的 React.createElement()
語法之後,就能夠正常的在 JavaScript runtime 中執行了。
Babel 是 JavaScript 社群中最主流且熱門的 Source code transpiler,它可以幫助我們將 JavaScript 原始碼轉譯成另一種模樣的 JavaScript 原始碼,而其中的轉譯效果與邏輯可以完全自定義,藉由安裝各種社群提供的 Babel plugins 或自己撰寫 plugin 來達到。
Babel 在 JavaScript 領域的用途相當廣泛,像是:
將較新的 ES 語法轉換成比較舊的相同行為語法來讓一些比較舊的瀏覽器也能支援
例如:Arrow function 的語法轉換成傳統的匿名函式定義語法
// Babel input: ES6 arrow function
[1, 2, 3].map(n => n + 1);
// Babel output: ES5 equivalent
[1, 2, 3].map(function(n) {
return n + 1;
});
進行一些自定義語法的轉換
例如我們這次所需的 JSX ⇒ React.createElement()
轉換
相關 Babel 插件:https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#react-classic-runtime
在此我們就不特別花篇幅介紹 Babel 的使用教學,有需要的朋友可以參考前面系列文 Day 03:React 開發環境建置的門檻 當中提供的推薦參考資源。
另外,在系列文中出現的範例 CodeSandbox 中都已經內建好相關的開發環境設置,可以安心的直接使用 JSX 語法。
你可以到 Babel 提供的即時轉換 Playground 試玩 JSX 被轉譯的效果:Babel REPL
import React from 'react'
?在某些 React 17 之前(≤ 16)版本的 React 專案中,你可能會發現如果在有使用到 JSX 語法的 JavaScript 檔案中沒有寫 import React from 'react'
的話,會在瀏覽器 runtime 執行遇到一個 React is not defined.
的錯誤:
這是因為在 React 17 之前,搭配的 babel JSX transformer 是會真的將 JSX 白紙黑字的轉換成 React.createElement()
這種語法然後輸出。因此 transformer 其實是假定這段程式碼中的變數環境中已經有 React
這個變數的存在,並且裡面有 createElement()
這個方法可以呼叫的:
// Babel input file
import React from 'react';
const reactElement = <button id="button1">I am a button</button>;
// Babel output file
import React from 'react';
const reactElement = React.createElement(
'button',
{ id: 'button1' },
'I am a button'
);
因此當你沒有在這個檔案先 import 好 React
的話,就會導致 runtime 在實際執行到這行時因為找不到 React
這個變數而噴錯。
不過從 React 17 開始,React 官方與 Babel 合作並支援了新的 JSX transformer,可以讓我們不再需要針對使用 JSX 撰寫這種配合用的 import React 程式碼了。詳情可以參考官方部落格的這篇文章。
由於本系列文所對應的 React 版本為 18 以上,因此在後續的範例中我們都會省略這種因使用 JSX 而 import React 的動作。
我們會在下一篇文章當中進一步解析 JSX 語法的一些核心特性與規則。
在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~
《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》
目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:
天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695
博客來(平裝版):
https://www.books.com.tw/products/0010982322
momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845